﻿using System;
using System.Collections.Generic;
using System.Text;
using GarageGames.Torque.Core;
using System.Xml;
using GarageGames.Torque.Core.Xml;
using GarageGames.Torque.GameUtil;
using System.Threading;
using System.IO;
using Microsoft.Xna.Framework;
using System.Reflection;
using GarageGames.Torque.Materials;
using System.Diagnostics;
using GarageGames.Torque.SceneGraph;
using GarageGames.Torque.XNA;
using GarageGames.Torque.GFX;
using System.Xml.Linq;
using GarageGames.Torque.Util;



namespace GarageGames.Torque.GameUtil
{
    /// <summary>
    /// (DBC)
    /// 
    /// Provides support for loading scene files ascynchronously.
    /// 
    /// NOTE: multiple scene files can be queued up for loading and they will be loaded one
    /// after the other.
    /// 
    /// NOTE: this class does not do anything special about unloading scenes.
    /// 
    /// NOTE: there is currently no progress counter to track loading progress.
    /// </summary>
    public class AsyncSceneLoader : GameComponent
    {
        private const string STR_CannotLoadThisLevelVersion = "Cannot load this level version: ";
        private const string STR_InvalidLevelFileName = "Invalid level file name: ";
        protected enum EnumState { WaitingForTasking, LoadFile, ParseXml, DeserializeXml, LoadSceneData, LoadMaterials, LoadObjects, PreloadMaterials, FinalizeLoading };

        protected static TorqueXmlDeserializer _deserializer = new TorqueXmlDeserializer();

        protected EnumState _state = EnumState.WaitingForTasking;
        protected Queue<SceneLoadingInfo> _sceneLoadingQueue = new Queue<SceneLoadingInfo>();

        protected SceneLoadingInfo _sceneInfo;
        protected TorqueSceneData _sceneData;
        protected SceneRenderState srs;
        protected string _xmlString;

        protected bool _finishedLoadingFile;
        protected bool _finishedParsingXML;
        protected bool _finishedDeserializingXML;
        protected bool _finishedLoadingObjects;

        protected float _timeout;
        protected long _timeoutInTicks;
        protected long _startTimeInTicks;

        protected List<object> _objectsToLoad;
        protected int _nextObjectToLoad;

        public float TimeOut
        {
            get { return _timeout; }

            set
            {
                _timeout = value;
                _timeoutInTicks = (long)((double)_timeout * (double)Stopwatch.Frequency);
            }
        }


        public AsyncSceneLoader(Game game)
            : base(game)
        {
            TimeOut = 0.5f * 1.0f / 60.0f; // default to half a frame of processing time (if running at 60fps) - NOTE: this only applies to LoadObjects();(And Preload Materials: MATTH) everything else is either on a separate hardware thread or done in a single game tick
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);

            _sceneLoadingQueue.Clear();
            _sceneLoadingQueue = null;
            _xmlString = null;
            _sceneInfo = null;
            _sceneData = null;
            _objectsToLoad = null;
        }

        public AsyncSceneLoaderResult LoadScene(string sceneFileName, List<Assembly> extraAssemblies, AsyncCallback callback, object asyncObject)
        {
            _startTimeInTicks = Stopwatch.GetTimestamp();

            SceneLoadingInfo sceneInfo = new SceneLoadingInfo()
                {
                    SceneFileName = sceneFileName,
                    Xml = null,
                    Callback = callback,
                    Result = new AsyncSceneLoaderResult(asyncObject),
                    ExtraAssemblies = extraAssemblies,
                };

            _sceneLoadingQueue.Enqueue(sceneInfo);

            return sceneInfo.Result;
        }

        public AsyncSceneLoaderResult LoadScene(XDocument xml, string sceneName, List<Assembly> extraAssemblies, AsyncCallback callback, object asyncObject)
        {
            _startTimeInTicks = Stopwatch.GetTimestamp();

            SceneLoadingInfo sceneInfo = new SceneLoadingInfo()
                {
                    SceneFileName = sceneName,
                    Xml = xml,
                    Callback = callback,
                    Result = new AsyncSceneLoaderResult(asyncObject),
                    ExtraAssemblies = extraAssemblies
                };

            _sceneLoadingQueue.Enqueue(sceneInfo);

            return sceneInfo.Result;
        }

        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);

            switch (_state)
            {
                case EnumState.WaitingForTasking:
                    ProcessWaitingForTasking();
                    break;

                case EnumState.LoadFile:
                    ProcessLoadFile();
                    break;

                case EnumState.ParseXml:
                    ProcessParseXml();
                    break;

                case EnumState.DeserializeXml:
                    ProcessDeserializeXml();
                    break;

                case EnumState.LoadSceneData:
                    ProcessLoadSceneData();
                    break;

                case EnumState.LoadMaterials:
                    ProcessLoadMaterials();
                    break;

                case EnumState.LoadObjects:
                    ProcessLoadObjects();
                    break;

                case EnumState.PreloadMaterials:
                    ProcessPreloadMaterials();
                    break;

                case EnumState.FinalizeLoading:
                    ProcessFinalizeLoading();
                    break;

                default:
                    Assert.Fatal(false, "Unrecognized state: " + _state);
                    break;
            }
        }

        protected virtual void ProcessFinalizeLoading()
        {
            // nothing to do here - nothing to tick as the final step is all in a single game tick (necessarily, or things could get messed up)
            MoveToNextState();
        }

        protected virtual void ProcessLoadObjects()
        {
            TickObjectLoading();

            if (_finishedLoadingObjects)
            {
                MoveToNextState();
            }
        }

        protected virtual void ProcessPreloadMaterials()
        {
            TickMaterialPreloading(srs);

            if (_finishedLoadingObjects)
            {
                MoveToNextState();
            }
        }

        protected virtual void ProcessLoadMaterials()
        {
            TickObjectLoading();

            if (_finishedLoadingObjects)
            {
                MoveToNextState();
            }
        }

        protected virtual void ProcessLoadSceneData()
        {
            TickObjectLoading();

            if (_finishedLoadingObjects)
            {
                MoveToNextState();
            }
        }

        protected virtual void ProcessDeserializeXml()
        {
            if (_finishedDeserializingXML)
            {
                MoveToNextState();
            }
        }

        protected virtual void ProcessParseXml()
        {
            if (_finishedParsingXML)
            {
                MoveToNextState();
            }
        }

        protected virtual void ProcessLoadFile()
        {
            if (_finishedLoadingFile)
            {
                MoveToNextState();
            }
        }

        protected virtual void ProcessWaitingForTasking()
        {
            // if there is a scene queued for loading then start loading it
            if (_sceneLoadingQueue.Count > 0)
            {
                _sceneInfo = _sceneLoadingQueue.Dequeue();
                MoveToNextState();
            }
        }

        protected virtual void MoveToNextState()
        {
            switch (_state)
            {
                case EnumState.WaitingForTasking:
                    if (_sceneInfo.Xml != null)
                    {
                        // NOTE: the scene file name provided will be used to map the scene data in the scene loader's dictionary of loaded scenes, so we need to make sure there is a name provided
                        Assert.Fatal(_sceneInfo.SceneFileName != null && _sceneInfo.SceneFileName != string.Empty, "Xml document can not be loaded as a 'scene file' without also giving a name to reference the scene by.");
                        _state = EnumState.DeserializeXml;
                        StartDeserializeXml();
                    }
                    else
                    {
                        Assert.Fatal(_sceneInfo.SceneFileName != null && _sceneInfo.SceneFileName != string.Empty, "No scene file specified!");
                        _state = EnumState.LoadFile;
                        StartLoadFile();
                    }
                    break;

                case EnumState.LoadFile:
                    _state = EnumState.ParseXml;
                    StartParseXml();
                    break;

                case EnumState.ParseXml:
                    _state = EnumState.DeserializeXml;
                    StartDeserializeXml();
                    break;

                case EnumState.DeserializeXml:
                    _state = EnumState.LoadSceneData;
                    StartLoadSceneData();
                    break;

                case EnumState.LoadSceneData:
                    _state = EnumState.LoadMaterials;
                    StartLoadMaterials();
                    break;

                case EnumState.LoadMaterials:
                    _state = EnumState.LoadObjects;
                    StartLoadObjects();
                    break;

                case EnumState.LoadObjects:
                    // preload materials
                    if (_sceneData.PreloadMaterials)
                    {
                        _state = EnumState.PreloadMaterials;
                        StartPreloadMaterials();
                    }
                    else
                    {
                        _state = EnumState.FinalizeLoading;
                        StartFinalizeLoading();
                    }
                    break;

                case EnumState.PreloadMaterials:
                    _state = EnumState.FinalizeLoading;
                    StartFinalizeLoading();
                    break;

                case EnumState.FinalizeLoading:
                    _state = EnumState.WaitingForTasking;
                    StartWaitingForTasking();
                    break;

                default:
                    Assert.Fatal(false, "Unrecognized state: " + _state);
                    break;
            }
        }

        protected virtual void QuitLoadingEarly()
        {
            _state = EnumState.WaitingForTasking;   // jump straight back to the waiting state
            StartWaitingForTasking();
        }

        protected virtual void StartWaitingForTasking()
        {
            // null out all variables used in the scene loading process so we aren't preventing anything getting garbage collected
            _xmlString = null;
            _sceneInfo = null;
            _sceneData = null;
            _objectsToLoad = null;
        }

        protected virtual void StartFinalizeLoading()
        {
            // preload materials
            /*
            if (_sceneData.PreloadMaterials)
            {
                // helps to reduce startup hitching
                MaterialManager.PreloadMaterials(null);
            }
            */

            // call the scenedata.Onloaded delegate if there is one
            if (_sceneData.OnLoaded != null)
                _sceneData.OnLoaded();

            // add scene to loaded list (will also automatically set it as being the last loaded scene)
            (this.Game as TorqueGame).SceneLoader.RegisterSceneAsLoaded(_sceneInfo.SceneFileName, _sceneData);

            // output some info
            Debug.WriteLine("Scene loaded in (seconds) " + (float)(((double)Stopwatch.GetTimestamp() - (double)_startTimeInTicks)) / (double)Stopwatch.Frequency);

            // set the scene as finished loading now
            _sceneData.IsAsyncLoading = false;

            // call the sceneloader.Onloaded delegate if there is one
            if ((this.Game as TorqueGame).SceneLoader.OnSceneLoaded != null)
                (this.Game as TorqueGame).SceneLoader.OnSceneLoaded(_sceneInfo.SceneFileName, _sceneData);

            // update the async result
            _sceneInfo.Result.MarkAsCompleted();
            _sceneInfo.Result.SetSceneData(_sceneData);

            // call the async callback if there is one
            if (_sceneInfo.Callback != null)
            {
                _sceneInfo.Callback(_sceneInfo.Result);
            }
        }

        protected virtual void StartLoadFile()
        {
            SceneLoader sceneLoader = (this.Game as TorqueGame).SceneLoader;

            // if scene already loaded then nothing to do
            if (sceneLoader.IsLoaded(_sceneInfo.SceneFileName))
            {
                sceneLoader.LastLoadedScene = _sceneData;
                QuitLoadingEarly();
                return;
            }

            // we'll load the file using a separate hardware thread that we will monitor for completion
            _finishedLoadingFile = false;

            // create the thread and the code that will run when the thread is started
            ThreadStart ThreadStarter = delegate
            {
#if XBOX
                // On Xbox we can set the thread to run on a specific core so the file loading doesn't interrupt any loading animations or other processes as much
                // Alternatively if needed we could switch to a tick based loading that reads a few bytes from the file at a time so we can precisely control
                // how much time is spent loading the file during each game tick.
                Thread.CurrentThread.SetProcessorAffinity(4);
#endif
                // load the file into a text string - we don't catch any exceptions - we want to be screamed at if the file fails to load as this is a scene file we are loading and it *should* be there or something is really messed up
                StreamReader streamReader = new StreamReader(_sceneInfo.SceneFileName);
                _xmlString = streamReader.ReadToEnd();
                streamReader.Close();

                // finished
                _finishedLoadingFile = true;
            };

            Thread myThread = new Thread(ThreadStarter);

            // start the thread
            myThread.Start();
        }

        protected virtual void StartLoadObjects()
        {
            // loading objects is done asynchronously, but not on a separate hardware thread (the torque database is NOT thread safe)
            _objectsToLoad = _sceneData.Objects;
            _nextObjectToLoad = 0;
            _finishedLoadingObjects = false;
        }

        protected virtual void StartPreloadMaterials()
        {
            if (srs == null)
            {
                srs = new SceneRenderState();
                srs.SceneGraph = null;
                srs.Gfx = GFXDevice.Instance;
                //I pass out the Scene Render State so that it can be referenced for dynamicly changing Materials (I created a static property in the material manager: MATT H)
                MaterialManager.SRS = srs;
            }

            _objectsToLoad = _sceneData.Materials;
            _nextObjectToLoad = 0;
            _finishedLoadingObjects = false;
        }

        protected virtual void StartLoadMaterials()
        {
            // loading objects is done asynchronously, but not on a separate hardware thread (the torque database is NOT thread safe)
            _objectsToLoad = _sceneData.Materials;
            _nextObjectToLoad = 0;
            _finishedLoadingObjects = false;
        }

        protected virtual void StartLoadSceneData()
        {
            // set the current scenegraph to the one for the scene being loaded - objects will automatically be added into this scenegraph
            BaseSceneGraph ourSceneGraph = (BaseSceneGraph)ListUtils.Find(_sceneData.SceneData, delegate(object obj) { return obj is BaseSceneGraph; });

            if (ourSceneGraph != null) // Be sure not to put a null into it!
                TorqueObjectDatabase.Instance.CurrentSceneGraph = ourSceneGraph;

            // loading objects is done asynchronously, but not on a separate hardware thread (the torque database is NOT thread safe)
            _objectsToLoad = _sceneData.SceneData;
            _nextObjectToLoad = 0;
            _finishedLoadingObjects = false;
        }

        protected virtual void StartParseXml()
        {
            Assert.Fatal(_xmlString != null && _xmlString != string.Empty, "Xml string is empty");

            // we'll parse the xml using a separate hardware thread that we will monitor for completion
            _finishedParsingXML = false;

            // create the thread and the code that will run when the thread is started
            ThreadStart ThreadStarter = delegate
            {
#if XBOX
                Thread.CurrentThread.SetProcessorAffinity(4);
#endif
                // parse the xml into a document object - not catching any exceptions here as we want to be shouted at if the scene file is invalid; something is very wrong if that is the case
                XDocument d = XDocument.Parse(_xmlString);
                _sceneInfo.Xml = d;

                // finished
                _finishedParsingXML = true;
            };

            Thread myThread = new Thread(ThreadStarter);

            // start the thread
            myThread.Start();
        }

        protected virtual void StartDeserializeXml()
        {
            Assert.Fatal(_sceneInfo.Xml != null, "No xml document provided for deserializing");

            SceneLoader sceneLoader = (this.Game as TorqueGame).SceneLoader;

            // create new scene
            _sceneData = new TorqueSceneData();
            _sceneData.IsAsyncLoading = true;
            _sceneData.PreloadMaterials = true;
            _sceneData.Deserializer = _deserializer;

            // do some checks and create some stuff prior to deserializing
            Assert.Fatal(GarageGames.Torque.XNA.TorqueEngineComponent.Instance.ExecutableAssembly != null, "TorqueSceneData.Load - ExecutableAssembly not set.");
            Assert.Fatal(_sceneData.AllowReload || !_sceneData.Loaded, "TorqueSceneData.Load - Level already loaded, Unload it first.");

            if (_sceneData.Loaded)
            {
                if (_sceneData.AllowReload)
                {
                    _sceneData.Reset();
                }
                else
                {
                    QuitLoadingEarly();
                    return;
                }
            }

            _sceneData.Loaded = true;

            if (_sceneInfo.SceneFileName == null || _sceneInfo.SceneFileName == String.Empty)
                throw new Exception(STR_InvalidLevelFileName + _sceneInfo.SceneFileName);

            if (_sceneData.CreateLevelFolder)
            {
                // create a folder for game objects that are created after this level is loaded
                _sceneData.CreateNewFolder();
                _sceneInfo.Folder = TorqueObjectDatabase.Instance.CurrentFolder;
            }

            TorqueXmlDeserializer d = _sceneData.Deserializer;

            //d.LoadTypesFromAssemblies = true;
            d.DefaultTypeMap = _sceneData.DefaultTypeMap;
            List<Assembly> searchAssemblies = d.Assemblies;

            if (_sceneInfo.ExtraAssemblies != null)
                foreach (Assembly a in _sceneInfo.ExtraAssemblies)
                    if (!searchAssemblies.Contains(a))
                        searchAssemblies.Add(a);

            // start deserialization - note: it will use a separate hardware thread
            DoDeserialization();
        }

        protected virtual void DoDeserialization()
        {
            // we'll deserialize the xml using a separate hardware thread that we will monitor for completion
            _finishedDeserializingXML = false;

            // create the thread and the code that will run when the thread is started
            ThreadStart ThreadStarter = delegate
            {
#if XBOX
                Thread.CurrentThread.SetProcessorAffinity(4);
#endif
                // tell the TX deserializer to deserialize the xml document object into the scene data instance we created earlier
                _sceneData.Deserializer.Process(_sceneInfo.Xml, _sceneData);

                if (_sceneData.Version != TorqueSceneData.RequiredVersion)
                    throw new Exception(STR_CannotLoadThisLevelVersion + _sceneData.Version);

                // finished
                _finishedDeserializingXML = true;
            };

            Thread myThread = new Thread(ThreadStarter);

            // start the thread
            myThread.Start();
        }

        protected virtual void TickObjectLoading()
        {
            long lastTickCount = Stopwatch.GetTimestamp();
            int count = _objectsToLoad.Count;

            // DBC - HANDLE NULL ASSET - we don't want to load textures that aren't needed
            List<object> unrequired = new List<object>(_objectsToLoad.Count);

            // load as many objects as we can before the timeout is reached
            for (int i = _nextObjectToLoad; i < count; i++)
            {
                object obj = _objectsToLoad[i];

                // call OnLoad on the object if applicable, before doing anything else with it.
                TorqueBase tbaseObj = obj as TorqueBase;

                if (tbaseObj != null)
                {
                    // DBC - HANDLE NULL ASSETS - check if the object should not really be loaded and should in fact be removed
                    if ((tbaseObj as TorqueBase).CanLoad())
                    {
                        tbaseObj.SceneData = _sceneData;    // NOTE: we set the scenedata property so that the IsAsyncLoading flag will return true until we actually have finished loading this scene
                        tbaseObj.OnLoaded();
                    }
                    else
                    {
                        unrequired.Add(tbaseObj);
                        continue;
                    }
                }

                // handle TorqueObjects
                TorqueObject tObj = obj as TorqueObject;

                if (tObj != null)
                {
                    tObj.Folder = _sceneInfo.Folder;    // make sure the object is in the right folder - we are loading objects asynchronously so the folder could get changed; we use the stored one therefore and set it explicitly on the object

                    // if the object is not a template, register it now
                    if (!tObj.IsTemplate && !tObj.IsRegistered)
                        TorqueObjectDatabase.Instance.Register(tObj);
                }

                // if our time is up then let the rest of the game do some processing now
                if (Stopwatch.GetTimestamp() - lastTickCount >= _timeoutInTicks)
                {
                    _nextObjectToLoad = i + 1;  // remember the next object to load
                    return;
                }
            }

            // DBC - HANDLE NULL ASSET - remove any objects that aren't needed
            foreach (object obj in unrequired)
            {
                //Debug.WriteLine("NOT loading instance of " + obj.GetType());
                _objectsToLoad.Remove(obj);
            }

            // finished
            _finishedLoadingObjects = true;
        }

        protected virtual void TickMaterialPreloading(SceneRenderState srs)
        {
            long lastTickCount = Stopwatch.GetTimestamp();
            int count = _objectsToLoad.Count;

            bool wasPreload = srs.IsPreloadPass;
            srs.IsPreloadPass = true;
            // PINO: The foreach loop takes more time and resources 
            //       than a simple for loop
            // foreach (RenderMaterial m in _materials)
            for (int i = _nextObjectToLoad; i < count; i++)
            {
                object obj = _objectsToLoad[i];
                RenderMaterial tRenderMat = obj as RenderMaterial;

                //tRenderMat.SetupEffect(srs, null);
                while (tRenderMat.SetupPass(srs, null)) ;
                //tRenderMat.CleanupEffect();

                // if our time is up then let the rest of the game do some processing now
                if (Stopwatch.GetTimestamp() - lastTickCount >= _timeoutInTicks / 10)
                {
                    _nextObjectToLoad = i + 1;  // remember the next object to load
                    return;
                }
            }
            srs.IsPreloadPass = wasPreload;

            // finished
            _finishedLoadingObjects = true;
        }




        protected class SceneLoadingInfo
        {
            public string SceneFileName;
            public XDocument Xml;
            public AsyncSceneLoaderResult Result;
            public AsyncCallback Callback;
            public List<Assembly> ExtraAssemblies;
            public TorqueFolder Folder;
        }
    }



    public class AsyncSceneLoaderResult : IAsyncResult
    {
        protected bool _isCompleted = false;
        protected object _asyncObject = null;
        protected TorqueSceneData _sceneData = null;

        public virtual object AsyncState { get { return _asyncObject; } }
        public virtual WaitHandle AsyncWaitHandle { get { return null; } }
        public virtual bool CompletedSynchronously { get { return false; } }
        public virtual bool IsCompleted { get { return _isCompleted; } }
        public virtual TorqueSceneData SceneData { get { return _sceneData; } }


        public AsyncSceneLoaderResult(object asyncObject)
        {
            _asyncObject = asyncObject;
        }

        public void MarkAsCompleted()
        {
            _isCompleted = true;
        }

        public void SetSceneData(TorqueSceneData sceneData)
        {
            _sceneData = sceneData;
        }
    }
}
